1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 */ 6 module code_checker.engine.builtin.clang_tidy_classification; 7 8 public import code_checker.engine.types : Severity; 9 10 version (unittest) { 11 import unit_threaded : shouldEqual, shouldBeTrue; 12 } 13 14 @safe: 15 16 struct SeverityColor { 17 import colorlog : Color, Background, Mode; 18 19 Color c = Color.white; 20 Background bg = Background.black; 21 Mode m; 22 } 23 24 immutable Severity[string] diagnosticSeverity; 25 immutable SeverityColor[Severity] severityColor; 26 27 shared static this() @trusted { 28 import logger = std.experimental.logger; 29 import std.algorithm : filter; 30 import std.array : empty; 31 import std.conv : to; 32 import std.file : thisExePath, readText; 33 import std.json : parseJSON, JSONType; 34 import std.path : buildPath, dirName; 35 import std..string : split, toLower, startsWith; 36 37 const clangTidyPath = buildPath(thisExePath.dirName.dirName, "etc", 38 "code_checker", "clang-tidy.json"); 39 auto json = parseJSON(readText(clangTidyPath)); 40 41 import colorlog : Color, Background, Mode; 42 43 foreach (a; json["labels"].object.byKeyValue.filter!(a => a.value.type == JSONType.ARRAY)) { 44 Severity s = () { 45 foreach (v; a.value.array) { 46 auto splt = v.str.split(":"); 47 if (!splt.empty && splt[0] == "severity") { 48 try { 49 auto r = splt[1].toLower.to!Severity; 50 return r; 51 } catch (Exception e) { 52 } 53 } 54 } 55 return Severity.min; 56 }(); 57 58 if (a.key.startsWith("core.")) 59 diagnosticSeverity["clang-analyzer-" ~ a.key] = s; 60 else 61 diagnosticSeverity[a.key] = s; 62 } 63 64 // dfmt off 65 severityColor = [ 66 Severity.style: SeverityColor(Color.lightCyan, Background.black, Mode.none), 67 Severity.low: SeverityColor(Color.lightBlue, Background.black, Mode.bold), 68 Severity.medium: SeverityColor(Color.lightYellow, Background.black, Mode.none), 69 Severity.high: SeverityColor(Color.red, Background.black, Mode.bold), 70 Severity.critical: SeverityColor(Color.magenta, Background.black, Mode.bold), 71 ]; 72 // dfmt on 73 } 74 75 struct CountErrorsResult { 76 import code_checker.engine.types : Severity; 77 78 private { 79 int total; 80 int[Severity] score_; 81 int suppressedWarnings; 82 } 83 84 /// Returns: the score when summing up the found occurancies. 85 int score() @safe pure nothrow const @nogc scope { 86 int sum; 87 // just chose some numbers. The intent is that warnings should be a high penalty 88 foreach (kv; score_.byKeyValue) { 89 final switch (kv.key) { 90 case Severity.style: 91 sum -= kv.value; 92 break; 93 case Severity.low: 94 sum -= kv.value * 2; 95 break; 96 case Severity.medium: 97 sum -= kv.value * 5; 98 break; 99 case Severity.high: 100 sum -= kv.value * 10; 101 break; 102 case Severity.critical: 103 sum -= kv.value * 100; 104 break; 105 } 106 } 107 108 return sum; 109 } 110 111 void put(const Severity s) { 112 total++; 113 114 if (auto v = s in score_) 115 (*v)++; 116 else 117 score_[s] = 1; 118 } 119 120 void setSuppressed(const int v) { 121 suppressedWarnings = v; 122 } 123 124 auto toRange() const { 125 import std.algorithm : map, sort; 126 import std.array : array; 127 import std.format : format; 128 129 return score_.byKeyValue 130 .array 131 .sort!((a, b) => a.key > b.key) 132 .map!(a => format("%s %s", a.value, a.key)); 133 } 134 } 135 136 @("shall sort the error counts") 137 unittest { 138 import std.traits : EnumMembers; 139 import code_checker.engine.types : Severity; 140 import unit_threaded; 141 142 CountErrorsResult r; 143 foreach (s; [EnumMembers!Severity]) 144 r.put(s); 145 146 r.toRange.shouldEqual([ 147 "1 critical", "1 high", "1 medium", "1 low", "1 style" 148 ]); 149 } 150 151 struct DiagMessage { 152 Severity severity; 153 154 /// Filename that clang-tidy reported for the warning. 155 string file; 156 /// The diagnostic message such as file.cpp:2:3 error: some text [foo-check] 157 string diagnostic; 158 /// The trailing info such as fixits 159 string[] trailing; 160 } 161 162 struct StatMessage { 163 // Number of NOLINTs 164 int nolint; 165 } 166 167 /** Apply `fn` on the diagnostic messages. 168 * 169 * The return value from fn replaces the message. This makes it possible to 170 * rewrite a message if needed. 171 * 172 * Params: 173 * diagFn = mapped onto a diagnostic message 174 * statFn = statistics gathered from clang-tidy 175 * lines = an input range of lines to analyze for diagnostic messages 176 * w = output range that the resulting log is written to. 177 */ 178 void mapClangTidy(alias diagFn, Writer)(string[] lines, ref scope Writer w) { 179 import std.algorithm : startsWith; 180 import std.array : appender, Appender; 181 import std.conv : to; 182 import std.exception : ifThrown; 183 import std.range : put; 184 import std.regex : regex, matchFirst; 185 import std..string : startsWith; 186 187 void callDiagFnAndReset(ref DiagMessage msg, ref Appender!(string[]) app) { 188 msg.trailing = app.data; 189 app.clear; 190 if (diagFn(msg)) { 191 put(w, msg.diagnostic); 192 foreach (t; msg.trailing) 193 put(w, t); 194 } 195 196 msg = DiagMessage.init; 197 } 198 199 const re_error = regex( 200 `(?P<file>.*):\d*:\d*:.*(?P<kind>(error|warning)):.*\[(?P<severity>.*)\]`); 201 202 enum State { 203 none, 204 match, 205 partOfMatch, 206 newMatch, 207 } 208 209 State st; 210 auto app = appender!(string[])(); 211 DiagMessage msg; 212 foreach (l; lines) { 213 auto m_error = matchFirst(l, re_error); 214 215 final switch (st) { 216 case State.none: 217 if (m_error.length > 1) 218 st = State.match; 219 break; 220 case State.match: 221 if (m_error.length > 1) 222 st = State.newMatch; 223 else 224 st = State.partOfMatch; 225 break; 226 case State.partOfMatch: 227 if (m_error.length > 1) 228 st = State.newMatch; 229 break; 230 case State.newMatch: 231 if (m_error.length <= 1) 232 st = State.partOfMatch; 233 break; 234 } 235 236 final switch (st) { 237 case State.none: 238 break; 239 case State.match: 240 msg.severity = classify(m_error["severity"], m_error["kind"]); 241 msg.diagnostic = l; 242 msg.file = m_error["file"]; 243 break; 244 case State.partOfMatch: 245 app.put(l); 246 break; 247 case State.newMatch: 248 callDiagFnAndReset(msg, app); 249 250 msg.severity = classify(m_error["severity"], m_error["kind"]); 251 msg.diagnostic = l; 252 msg.file = m_error["file"]; 253 break; 254 } 255 } 256 257 msg.trailing = app.data; 258 if (st != State.none && diagFn(msg)) { 259 put(w, msg.diagnostic); 260 foreach (t; msg.trailing) 261 put(w, t); 262 } 263 } 264 265 void mapClangTidyStats(alias statFn)(string[] lines) { 266 import std.conv : to; 267 import std.exception : ifThrown; 268 import std.regex : regex, matchFirst; 269 270 const re_nolint = regex(`Supp.*\D(?P<nolint>\d+)\s*NOLINT.*`); 271 272 foreach (l; lines) { 273 auto m_nolint = matchFirst(l, re_nolint); 274 275 if (m_nolint.length > 1) { 276 auto nolint_cnt = m_nolint["nolint"].to!int.ifThrown(0); 277 statFn(StatMessage(nolint_cnt)); 278 } 279 } 280 } 281 282 @("shall filter warnings") 283 unittest { 284 import std.algorithm : startsWith; 285 import std.array : appender; 286 287 // dfmt off 288 string[] lines = [ 289 "gmock-matchers.h:3410:15: error: invalid case style for private method 'AnalyzeElements' [readability-identifier-naming,-warnings-as-errors]", 290 " MatchMatrix AnalyzeElements(ElementIter elem_first, ElementIter elem_last,", 291 " ^~~~~~~~~~~~~~~~", 292 " analyzeElements", 293 "gmock-matchers.h:3410:43: error: invalid case style for parameter 'elem_first' [readability-identifier-naming,-warnings-as-errors]", 294 " MatchMatrix AnalyzeElements(ElementIter elem_first, ElementIter elem_last,", 295 " ^~~~~~~~~~~", 296 " elemFirst", 297 "gmock-matchers2.h:3410:67: error: invalid case style for parameter 'elem_last' [readability-identifier-naming,-warnings-as-errors]", 298 " MatchMatrix AnalyzeElements(ElementIter elem_first, ElementIter elem_last,", 299 " ^~~~~~~~~~", 300 " elemLast", 301 ]; 302 // dfmt on 303 304 DiagMessage[] msgs; 305 bool diagMsg(DiagMessage msg) { 306 msgs ~= msg; 307 // skipping a message to see that it works 308 if (msgs.length == 1) 309 return false; 310 return true; 311 } 312 313 auto app = appender!(string[])(); 314 mapClangTidy!diagMsg(lines, app); 315 316 msgs.length.shouldEqual(3); 317 318 msgs[0].file.shouldEqual("gmock-matchers.h"); 319 msgs[0].diagnostic.startsWith("gmock-matchers.h:3410:15:").shouldBeTrue; 320 msgs[0].trailing.length.shouldEqual(3); 321 322 msgs[2].file.shouldEqual("gmock-matchers2.h"); 323 msgs[2].diagnostic.startsWith("gmock-matchers2.h:3410:67").shouldBeTrue; 324 msgs[2].trailing.length.shouldEqual(3); 325 326 app.data.length.shouldEqual(8); 327 } 328 329 @("shall report the number of suppressed warnings") 330 unittest { 331 // dfmt off 332 string[] lines = [ 333 "42598 warnings generated.", 334 "Suppressed 27578 warnings (27523 in non-user code, 55 NOLINT).", 335 "Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.", 336 ]; 337 // dfmt on 338 339 StatMessage stat; 340 void statFn(StatMessage msg) { 341 stat = msg; 342 } 343 344 // act 345 mapClangTidyStats!statFn(lines); 346 347 // assert 348 stat.nolint.shouldEqual(55); 349 } 350 351 /// Returns: the classification of the diagnostic message. 352 Severity classify(string diagnostic_msg, string kind) { 353 import std..string : startsWith; 354 355 if (kind == "error") 356 return Severity.critical; 357 358 if (auto v = diagnostic_msg in diagnosticSeverity) { 359 return *v; 360 } 361 362 // this is a fallback when new rules are added to clang-tidy but 363 // they haven't been thoroughly analyzed in 364 // `code_checker.engine.builtin.clang_tidy_classification`. 365 if (diagnostic_msg.startsWith("readability-")) 366 return Severity.style; 367 else if (diagnostic_msg.startsWith("clang-analyzer-")) 368 return Severity.high; 369 370 return Severity.medium; 371 } 372 373 /** 374 * Params: 375 * predicate = param is the classification of the diagnostic message. True means that it is kept, false thrown away 376 * Returns: a range of rules to inactivate that are below `s` 377 */ 378 auto filterSeverity(alias predicate)() { 379 import std.algorithm : filter, map; 380 381 // dfmt off 382 return diagnosticSeverity 383 .byKeyValue 384 .filter!(a => predicate(a.value)) 385 .map!(a => a.key); 386 // dfmt on 387 } 388 389 /// Returns: severity as a string with colors. 390 string color(Severity s) { 391 import std.conv : to; 392 static import colorlog; 393 394 SeverityColor sc; 395 396 if (auto v = s in severityColor) { 397 sc = *v; 398 } 399 400 return colorlog.color(s.to!string, sc.c).bg(sc.bg).mode(sc.m).toString; 401 }